using System;
using System.Collections.Generic;
using System.Reflection;
#if DEBUG
using Fadd.Tests;
using Xunit;
#endif

namespace Fadd
{
    /// <summary>
    /// General purpose class to get/set properties by specifying their name as text.
    /// </summary>
    /// <example>
    /// <code>
    /// User user = new User();
    /// Property.Set(user, "FirstName", "Jonas"); // same as user.FirstName = "Jonas";
    /// Property.Set(user, "CreatedAt", "2008-12-30 14:00"); // the CreatedAt property is of type DateTime, the Property class automatically converts it from string.
    /// Property.Set(user, "Age", "20"); // age is int, which means that it will automatically be converted.
    /// </code>
    /// </example>
    /// todo: Add the ability to extend the object with on additions.
    public static class Property
    {
        private const string Code =
            @"namespace Fadd {
public class FaddFTW : Fadd.IPropertiesObject
{
{PropertyTypes}

	public void Set(object item, string propertyName, object value)
	{
        Exception ex = null;
		{TypeName} obj = ({TypeName})item;
        try
        {
		switch (propertyName)
		{
{SetProperties}
            
            default:
                throw new ArgumentException(""Property '"" + propertyName + ""' is not found."");
		}
        }
        catch (InvalidCastException err)
        {
            ex = new InvalidCastException(""Failed to cast '"" + propertyName + ""' from '""  + value.GetType() + ""')"", err);
        }
        if (ex != null)
            throw ex;
	}

	public object Get(object item, string propertyName)
	{
		{TypeName} obj = ({TypeName})item;
		switch (propertyName)
		{
{GetProperties}
		}
        throw new ArgumentException(""Property '"" + propertyName + ""' is not found."");
	}
}}";
        private static readonly IDictionary<Type, IPropertiesObject> _assignerObjects = new Dictionary<Type, IPropertiesObject>();


        /// <summary>
        /// Assign a property value to the specified object.
        /// </summary>
        /// <param name="item">Object that has the property</param>
        /// <param name="propertyName">Name of the property</param>
        /// <param name="value">Value, this method will try to cast incorrect types (and even try to parse certain types).</param>
        /// <exception cref="ArgumentException">If value is not correct (i.e. null when null is not accepted etc)</exception>
        /// <exception cref="InvalidOperationException">If assigner object could not be generated.</exception>
        /// <exception cref="InvalidCastException">If assigner object can't cast to the correct type.</exception>
        public static void Set(object item, string propertyName, object value)
        {
            Check.Require(item, "item");
            Check.NotEmpty(propertyName, "propertyName");

            IPropertiesObject obj = GetObject(item);
            if (obj == null)
                throw new InvalidOperationException("Failed to find object converter for " + item.GetType().FullName);

            obj.Set(item, propertyName, value);
        }

        /// <summary>
        /// Get a property value from the specified object.
        /// </summary>
        /// <param name="item">Object that has the property</param>
        /// <param name="propertyName">Name of the property</param>
        /// <returns>property value</returns>
        /// <exception cref="ArgumentException">If value is not correct (i.e. null when null is not accepted etc)</exception>
        /// <exception cref="InvalidOperationException">If assigner object could not be generated.</exception>
        /// <exception cref="InvalidCastException">If assigner object can't cast to the correct type.</exception>
        public static object Get(object item, string propertyName)
        {
            Check.Require(item, "item");
            Check.NotEmpty(propertyName, "propertyName");

            IPropertiesObject obj = GetObject(item);
            if (obj == null)
                throw new InvalidOperationException("Failed to find object converter for " + item.GetType().FullName);

            return obj.Get(item, propertyName);
        }

#if DEBUG
        [Fact]
        private static void TestAssigner()
        {
            PropertyTestObj1 obj = new PropertyTestObj1();
            PropertyTestObj2 obj2 = new PropertyTestObj2();
            Set(obj, "Age", "10");
            Set(obj, "CreatedAt", "2008-10-11 22:00");
            Set(obj, "FirstName", "jonas");
            Set(obj, "Obj2", obj2);
            Assert.Throws(typeof (ArgumentException), delegate { Set(obj, "obj2", null); });
            Assert.Equal(10, (int)Get(obj, "Age"));
            Assert.Equal(new DateTime(2008, 10, 11, 22, 00, 00), (DateTime) Get(obj, "CreatedAt"));
            Assert.Equal("jonas", (string)Get(obj, "FirstName"));
            Assert.Throws(typeof(ArgumentException), delegate { Get(obj, "obj2"); });
            Assert.Equal(obj2, (PropertyTestObj2)Get(obj, "Obj2"));
        }
#endif
        private static IPropertiesObject GetObject(object item)
        {
            IPropertiesObject obj;
            Type type = item.GetType();

            lock (_assignerObjects)
            {
                if (_assignerObjects.TryGetValue(type, out obj))
                    return obj;
            }

            // generate set properties
            string sp = string.Empty;
            foreach (PropertyInfo info in type.GetProperties())
            {
                if (!info.CanWrite)
                    continue;
                sp += "			case \"" + info.Name + "\":\r\n";
                sp += "				if (value is " + info.PropertyType + ")\r\n";
                sp += "					obj." + info.Name + " = (" + info.PropertyType + ")value;\r\n";
                if (info.PropertyType == typeof(DateTime))
                {
                    sp += "				else if (value is string)\r\n";
                    sp += "					obj." + info.Name + " = DateTime.Parse((string)value);\r\n";
                }
                sp += "				else\r\n";
                sp += "					obj." + info.Name + " = (" + info.PropertyType + ")Convert.ChangeType(value, " + info.Name + "Type);\r\n";
                sp += "				break;\r\n";
            }
            // generate get properties.
            string gp = string.Empty;
            foreach (PropertyInfo info in type.GetProperties())
            {
                if (!info.CanRead)
                    continue;
                gp += "			case \"" + info.Name + "\":\r\n";
                gp += "				return obj."+info.Name+";\r\n";
            }

            string propTypes = string.Empty;
            foreach (PropertyInfo info in type.GetProperties())
                propTypes += "\tprivate static readonly Type " + info.Name + "Type = typeof(" + info.PropertyType.Name + ");\r\n";

            Compiler comp = new Compiler();
            comp.Add(item.GetType());
            comp.Add(typeof(IPropertiesObject));
            foreach (PropertyInfo info in type.GetProperties())
            {
                comp.Add(info.PropertyType);
                foreach (Type argument in info.PropertyType.GetGenericArguments())
                    comp.Add(argument);
            }

            string code = Code.Replace("{TypeName}", type.Name);
            code = code.Replace("{GetProperties}", gp);
            code = code.Replace("{SetProperties}", sp);
            code = code.Replace("{PropertyTypes}", propTypes);
            comp.Compile(code);

            obj = comp.CreateInstance<IPropertiesObject>();
            lock (_assignerObjects)
            {
                if (!_assignerObjects.ContainsKey(type))
                    _assignerObjects.Add(type, obj);
            }

            return obj;
        }
    }

    /// <summary>
    /// Base object for property assigner.
    /// </summary>
    public interface IPropertiesObject
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="item"></param>
        /// <param name="propertyName"></param>
        /// <param name="value"></param>
        void Set(object item, string propertyName, object value);
        /// <summary>
        /// 
        /// </summary>
        /// <param name="item"></param>
        /// <param name="propertyName"></param>
        /// <returns></returns>
        object Get(object item, string propertyName);
    }

}
